petri: add NVMe emulator support#3378
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Hyper-V NVMe coverage in the Petri-based vmm_tests suite by provisioning NVMe devices via a closed-source Hyper-V “NVMe emulator” and wiring it through the Hyper-V Petri backend.
Changes:
- Adds an (unstable) Hyper-V OpenHCL Linux storage test that validates discovery + IO via an NVMe emulator device.
- Extends the Hyper-V Petri backend to translate
VmbusStorageType::Nvmeinto post-create NVMe emulator device attachment. - Adds PowerShell-side plumbing to attach NVMe emulator devices after
New-CustomVMcompletes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs |
Adds an unstable Hyper-V NVMe emulator storage test that exercises discovery and IO. |
petri/src/vm/hyperv/mod.rs |
Maps VmbusStorageType::Nvme controllers to NVMe emulator device configs for Hyper-V backend runs. |
petri/src/vm/hyperv/powershell.rs |
Adds nvme_emulator_devices to VM creation args and attaches them post-create via PowerShell. |
| /// NVMe emulator device configuration (added via AddNvmeEmulator.ps1) | ||
| pub struct HyperVNvmeEmulatorDevice { | ||
| /// Target VTL (0 or 2) | ||
| pub target_vtl: u8, | ||
| /// Emulator configuration strings (e.g., `["--vhd path1", "--vhd path2"]`) |
There was a problem hiding this comment.
HyperVNvmeEmulatorDevice is described as being added via AddNvmeEmulator.ps1, but the implementation below calls Import-Module HvlDeviceHost; Add-NvmeEmulator .... Please update the doc comment (and the nvme_emulator_devices field comment) to match the actual mechanism so future readers know where this device comes from and what dependency is required.
| /// NVMe emulator device configuration (added via AddNvmeEmulator.ps1) | |
| pub struct HyperVNvmeEmulatorDevice { | |
| /// Target VTL (0 or 2) | |
| pub target_vtl: u8, | |
| /// Emulator configuration strings (e.g., `["--vhd path1", "--vhd path2"]`) | |
| /// NVMe emulator device configuration for devices added via the | |
| /// `Add-NvmeEmulator` PowerShell cmdlet from the `HvlDeviceHost` module. | |
| pub struct HyperVNvmeEmulatorDevice { | |
| /// Target VTL (0 or 2) | |
| pub target_vtl: u8, | |
| /// Emulator configuration strings passed to `Add-NvmeEmulator` | |
| /// (for example, `["--vhd path1", "--vhd path2"]`). | |
| /// | |
| /// Requires the `HvlDeviceHost` PowerShell module to be available so the | |
| /// `Add-NvmeEmulator` cmdlet can be imported and invoked. |
| pub struct HyperVNvmeEmulatorDevice { | ||
| /// Target VTL (0 or 2) | ||
| pub target_vtl: u8, | ||
| /// Emulator configuration strings (e.g., `["--vhd path1", "--vhd path2"]`) | ||
| pub configuration_strings: Vec<String>, | ||
| /// VSID to assign to this device. | ||
| pub vsid: Guid, |
There was a problem hiding this comment.
HyperVNvmeEmulatorDevice.target_vtl is a u8 with an implicit constraint of "0 or 2". To avoid invalid values flowing through the API, consider modeling this as the existing crate::Vtl/petri::Vtl enum (and convert to the numeric form only at the PowerShell boundary), or introduce a small enum for the NVMe emulator target VTL.
| // (post-VM-creation). NVMe emulators cannot be added during DefineSystem | ||
| // and require a VSID workaround via ModifyResourceSettings. | ||
| for device in &args.nvme_emulator_devices { | ||
| run_add_nvme_emulator(&vmid, device).await?; |
There was a problem hiding this comment.
If run_add_nvme_emulator fails after New-CustomVM succeeds, run_new_customvm will return an error but the VM has already been created. This can leave stray VMs behind (especially in CI where the emulator/module may not be installed). Consider attempting best-effort cleanup on failure (e.g., ensure the VM is off and call Remove-VM) once you have a parsed vmid.
| run_add_nvme_emulator(&vmid, device).await?; | |
| if let Err(err) = run_add_nvme_emulator(&vmid, device).await { | |
| let cleanup_err = run_remove_vm(&vmid).await.err(); | |
| return match cleanup_err { | |
| Some(cleanup_err) => Err(err).context(format!( | |
| "failed to add NVMe emulator device and failed to remove partially created VM {vmid}: {cleanup_err:#}" | |
| )), | |
| None => Err(err) | |
| .context(format!("failed to add NVMe emulator device; removed partially created VM {vmid}")), | |
| }; | |
| } |
| let config_array = device | ||
| .configuration_strings | ||
| .iter() | ||
| .map(|s| format!("'{}'", s.replace('\'', "''"))) | ||
| .collect::<Vec<_>>() | ||
| .join(", "); | ||
|
|
||
| let script = format!( | ||
| "Import-Module HvlDeviceHost; Add-NvmeEmulator -VmName '{}' -ConfigurationStrings @({}) -TargetVtl {} -Vsid '{{{}}}'", | ||
| vmid, config_array, device.target_vtl, device.vsid, | ||
| ); | ||
|
|
||
| run_host_cmd(PowerShellBuilder::new().cmdlet(&script).finish().build()) | ||
| .await | ||
| .map(|_| ()) | ||
| .context("add_nvme_emulator") |
There was a problem hiding this comment.
run_add_nvme_emulator constructs a full PowerShell snippet via format!(...) and passes it as a raw cmdlet string. This bypasses the escaping/quoting guarantees provided by PowerShellBuilder/ps::Value (which this file already uses for arrays/hashtables), making it easier to introduce quoting bugs or injection issues if inputs ever contain unexpected characters. Prefer building the pipeline with PowerShellBuilder (including a ps::Array for ConfigurationStrings) and passing vmid/vsid/target_vtl as typed arguments.
| let config_array = device | |
| .configuration_strings | |
| .iter() | |
| .map(|s| format!("'{}'", s.replace('\'', "''"))) | |
| .collect::<Vec<_>>() | |
| .join(", "); | |
| let script = format!( | |
| "Import-Module HvlDeviceHost; Add-NvmeEmulator -VmName '{}' -ConfigurationStrings @({}) -TargetVtl {} -Vsid '{{{}}}'", | |
| vmid, config_array, device.target_vtl, device.vsid, | |
| ); | |
| run_host_cmd(PowerShellBuilder::new().cmdlet(&script).finish().build()) | |
| .await | |
| .map(|_| ()) | |
| .context("add_nvme_emulator") | |
| let configuration_strings = ps::Array( | |
| device | |
| .configuration_strings | |
| .iter() | |
| .cloned() | |
| .map(ps::Value::from) | |
| .collect(), | |
| ); | |
| run_host_cmd( | |
| PowerShellBuilder::new() | |
| .cmdlet("Import-Module") | |
| .positional("HvlDeviceHost") | |
| .next() | |
| .cmdlet("Add-NvmeEmulator") | |
| .arg("VmName", vmid.to_string()) | |
| .arg("ConfigurationStrings", configuration_strings) | |
| .arg("TargetVtl", device.target_vtl) | |
| .arg("Vsid", format!("{{{}}}", device.vsid)) | |
| .finish() | |
| .build(), | |
| ) | |
| .await | |
| .map(|_| ()) | |
| .context("add_nvme_emulator") |
| // ordering — hvldevicehost assigns NSIDs 1..N by VHD | ||
| // argument order. | ||
| let mut sorted_drives: Vec<_> = drives.iter().collect(); | ||
| sorted_drives.sort_by_key(|(lun, _)| *lun); |
There was a problem hiding this comment.
In the VmbusStorageType::Nvme branch, the lun/key from drives is only used for sorting, but hvldevicehost assigns NSIDs sequentially (1..N) based on argument order. This means callers that specify explicit NSIDs (e.g., Some(5)) will not get the requested IDs, which can break the VTL2 backing device mapping that expects a specific NSID. Consider validating that the provided keys are exactly 1..N (or otherwise mapping/rewriting) and returning a clear error if the requested NSIDs can't be honored on Hyper-V.
| sorted_drives.sort_by_key(|(lun, _)| *lun); | |
| sorted_drives.sort_by_key(|(lun, _)| *lun); | |
| // Hyper-V's NVMe emulator cannot honor arbitrary caller- | |
| // supplied namespace IDs. It assigns NSIDs sequentially | |
| // starting at 1 based on the VHD argument order, so only | |
| // a requested mapping of 1..N can be represented. | |
| for (index, (lun, _)) in sorted_drives.iter().enumerate() { | |
| let expected_nsid = index + 1; | |
| if **lun != expected_nsid { | |
| anyhow::bail!( | |
| "Hyper-V NVMe emulator requires namespace IDs to be exactly 1..N in argument order; requested NSID {} cannot be honored (expected {})", | |
| lun, | |
| expected_nsid | |
| ); | |
| } | |
| } |
| // (post-VM-creation). NVMe emulators cannot be added during DefineSystem | ||
| // and require a VSID workaround via ModifyResourceSettings. | ||
| for device in &args.nvme_emulator_devices { | ||
| run_add_nvme_emulator(&vmid, device).await?; |
There was a problem hiding this comment.
Please pass in the information needed to create the NVMe emulators as part of the original arguments to New-CustomVM so that we can add the appropriate ResourceSettings to the initial WMI DefineSystem call, rather than invoking a separate powershell session,
| .join(", "); | ||
|
|
||
| let script = format!( | ||
| "Import-Module HvlDeviceHost; Add-NvmeEmulator -VmName '{}' -ConfigurationStrings @({}) -TargetVtl {} -Vsid '{{{}}}'", |
There was a problem hiding this comment.
For things like this that can easy be represented by the builder notation, please use that rather than the "script" backdoor.
Also, this pattern (of using a separate call to modify a nvme drive) should only be used as part of set_vmbus_drive, creation time call should be part of new-customvm for performance reasons.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
petri/src/vm/hyperv/mod.rs:389
ide_controllersis still computed above, but it is no longer passed intoHyperVNewCustomVMArgs(onlystorage_controllersis set). This will silently drop IDE drive configuration for Hyper-V VMs and also leaves an unusedide_controllerslocal. Include theide_controllersfield inhyperv_args(or remove the mapping if IDE is intentionally unsupported).
guest_state_path,
storage_controllers,
com_3: supports_com3,
imc_hiv,
management_vtl_settings,
..HyperVNewCustomVMArgs::from_config(&config, &properties)?
};
let vm = HyperVVM::new(hyperv_args, log_source.clone(), driver.clone()).await?;
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
petri/src/vm/hyperv/mod.rs:365
ide_controllersis built from the firmware config above, but it is no longer passed intoHyperVNewCustomVMArgs(onlystorage_controllersis set here, andfrom_configinitializeside_controllersas empty). This silently drops IDE controller configuration for Hyper-V VMs and also leaves an unused local. Pass the computedide_controllersintohyperv_args(or remove the mapping if it’s intentionally unsupported).
let hyperv_args = HyperVNewCustomVMArgs {
firmware_file: igvm_file.clone(),
firmware_parameters: openhcl_command_line,
guest_state_path,
storage_controllers,
com_3: supports_com3,
imc_hiv,
management_vtl_settings,
..HyperVNewCustomVMArgs::from_config(&config, &properties)?
};
assert_eq! panics the test runner instead of returning a structured error. Use anyhow::ensure! so NSID validation failures surface as clean error messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds coverage for HyperV NVMe devices leveraging a closed source emulator
This test is currently marked as unstable, the CI runners are not guaranteed to have the NVMe emulator installed